Observer- Ember.js入門(10)
渡辺です。
随分と空いてしまいましたが、Ember.jsを見捨てたワケではありません(笑)クローズドなプロダクトになりますが、Ember.jsを採用してアプリケーションを構築しています。Ember.jsは使いこなせるまでの学習コストは高いのですが、一度覚えてしまえば非常にサクサクとフロントサイドを構築できるでしょう。
さて、Emmber.js入門久しぶりのエントリーということで、今回はEmber.jsのObserverについて解説します。バインディング – Ember.js入門(5)でも触れましたが、Ember.jsではオブジェクトのプロパティとテンプレートの入出力を結びつけることができます。この時、オブジェクトのプロパティへのアクセスはgetメソッドやsetメソッドを使わなくてはなりませんでした。今回は、その裏側で何が行われているかを解説します。
Ember.Objectクラス
Observerについて解説する前に、Emberオブジェクトの基底クラスであるEmber.Objectクラスについて解説します。Ember.Objectクラスでは、プロパティへのgetter/setterによるアクセスやObserberの機能を提供します。Ember.jsで扱うRout, Controller, ViewなどはすべてEmber.Objectクラスのサブクラスです。Ember.jsでアプリケーション固有のクラスを定義する場合は、原則としてEmber.Objectを継承したクラスとして定義します。
クラスの定義
Ember.jsではEmber.Objectのサブクラスをextendメソッドを利用して、次のようにして定義します。
App.Person = Ember.Object.extend({ firstName: null, lastName: null });
App.Personクラスでは2つのプロパティを定義しています。勿論、メソッド(function)を宣言することもできます。
インスタンスの生成
インスタンスを生成する時は、createメソッドを利用します。例えば、App.Personクラスのインスタンスは、次のようにして生成します。
var person = App.Person.create({ firstName: 'Shuji', lastName: 'Watanabe' });
createメソッドのオプションを指定しない場合は、デフォルトの値で初期化されます。
プロパティの取得
インスタンスからプロパティを取得するにはgetメソッドを利用します。
person.get('firstName');
getメソッドなんてダサイと思う人が多いと思いますが、プロパティを取得する場合は必ずgetメソッドを使います。
プロパティの設定
インスタンスのプロパティを設定するにはsetメソッドを利用します。
person.set('lastName', 'WATANABE');
getメソッドと同様に、プロパティを設定する場合は必ずsetメソッドを使います。
インスタンスの初期化処理
インスタンスの生成時に処理を行いたい場合、initメソッドを再定義します。
App.Person = Ember.Object.extend({ firstName: null, lastName: null, init: function() { console.debug('Person#init', this.get('firstName')); } });
initメソッドは、インスタンスが生成され、初期パラメータによるプロパティの初期化が終わった後に実行されます。コンストラクタ的なタイミングとは異なるので注意してください。
このinitメソッドの再定義は、ControllerやRouteなどを初期化した後、1回だけ処理を行いたい場合によく利用します。例えば、次のようにApp.PersonsController生成時の処理を定義できます。
App.PersonsController = Ember.ArrayController.extend({ init: function() { console.debug('PersonsController#init'); // 初期化処理 } });
Ember.Observable
Ember.ObservableはオブジェクトにObserverとしての機能を追加するmixinです。ただし、Ember.Objectが既にEmber.Observableをmixinしているので、通常はEmber.Observableを宣言することはありません。
MVCとObserverパターン
Observerと聞いてはじめに思い浮かぶのはデザインパターンのObserverパターンではないでしょうか?
Ember.jsのObserverも、まさにデザインパターンのObserverパターンを便利に使うための機能です。そして、ObserverパターンはMVCと密接な関係にあります。
GUIアプリケーションでは、しばしばModelの状態の変更に伴いViewが再描画されます。例えば、Personオブジェクト(Model)のfirstNameが変更された時、firstNameに関連するテキストエリアが再描画されて最新情報を表示するでしょう。
このような処理をフロー制御で記述すると、「Modelの変更後、対応するViewを再描画」となります。このような関係がそれほど多くないならば問題となりません。しかし、Modelに多くのプロパティが定義されており、Viewには多くの表示エリアがあるとしたならば、コードの可読性は期待できないものとなります。
そこで、Modelのプロパティを変更したならば、予め登録されていたViewに対し変更通知を行い、各Viewがそれぞれ自身を再表示するという方針とします。こうする事で、Modelはシンプルな状態を保つことが可能となり、Viewは自身をModelに登録するだけで済むことになります。
このように対象のModelに対し、その状態を観察(observe)して処理を行うパターンがObserverパターンです。
より一般化した形のUMLではModelに相当するのがSubject(対象オブジェクト)で、Viewに相当するのがObserver(監視者)です。SubjectとObserverは、それぞれ監視対象と監視時の処理を行うことを責務としています。
Observerパターンでは、Subjectに対しObserverを登録(add)します。そして、Subjectの状態を変更したならば、全ての登録されたObserverに通知(notifyObservers)します。Observerは通知される(notify)とそれぞれの実装クラス固有の処理(テキストエリアを再描画するなど)することになります。
Ember.jsではこのようなObserberパターンを元にした仕組みが組み込まれており、Ember.Objsetのサブクラスにすれば簡単に利用できます。なお、JavaのSwingなどGUIアプリケーションフレームワークなどでは、ObserverをEventListenerと呼びますが、考え方は変わりません。
それでは実際にObserverを登録してみましょう。
addObserverメソッドによる登録
addObserverメソッドを利用することで、EmberオブジェクトにObserver functionを登録することができます。addObserverメソッドの第1引数はフックするプロパティのキー、第2引数はObserver functionを実行するObject、 第3引数に実行するfunctionを指定します。
例えば、firstNameが変更された場合に実行するObserver functionは次のように定義します。
var person = App.Person.create({ firstName: 'Shuji', lastName: 'Watanabe' }); person.addObserver('firstName', person, function() { console.debug('changed!', this.get('firstName')); });
Observerが登録されていることは、firstNameプロパティを設定することで確認出来ます。ただし、この時に必ずsetメソッドを使わなくてはなりません。setメソッドでプロパティを設定することで、Subject(person)は登録されているObserver(function)に変更を通知することができるのです。次のようにsetメソッドを実行するとコンソールにログが出力されます。
person.set('firstName', 'SHUJI');
Ember.jsではパフォーマンスも考慮してObserverへの通知を行っています。setメソッドが実行されたとしても、値が変更されない場合はObserver functionは実行されません。対象のプロパティが変更された時にObserverへ通知されます。
observesメソッドによる登録
addObserverメソッドはObserverパターンとして理解しやすいのですが、オブジェクトを作成した後にObserverを登録するのが面倒です。登録を忘れないようにするにはinitメソッドを使います。
App.Person = Ember.Object.extend({ firstName: null, lastName: null, init: function() { console.debug('Person#init'); this.addObserver('firstName', this, function() { console.debug('changed!', this.get('firstName')); }); } });
まだ手続き的過ぎてあまりイケてないコードです。
しかし、Ember.jsではfunctionのobservesメソッドを実行することで、Observerを宣言的に定義することができます。
App.Person = Ember.Object.extend({ firstName: null, lastName: null, firstNameChanged: function() { console.debug('changed!', this.get('firstName')); }.observes('firstName') });
functionの名前(firstNameChanged)は、特に制限はありません。Ember.jsで拡張されたfunctionのobservesメソッドを呼び出すところがポイントです。引数には監視対象のプロパティを指定します。この書式であれば、宣言的にObserverを定義することができます。このようにEmber.jsではfunctionを魔改造しています:)
まとめ
今回はEmberオブジェクトの根幹となるEmber.Objectクラスを中心に、バインディングのベースとなっているObserverを紹介しました。これらの仕組みは、Ember.jsでアプリケーションを構築していくときに基礎となる知識です。何故、setter/getterを利用することになっているのか?プロパティが変更された時に何が起こりえるか?などを自然に考えられるようになることは大切です。
次回はObserverの応用機能であるComputed Propertiesを紹介します。